﻿//---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation.  All rights reserved.
//
// File: RibbonTabPanel.cs
//
//---------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace Microsoft.Windows.Controls.Ribbon
{
    public class RibbonTabPanel : Panel, IScrollInfo
    {
        protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
        {
            // When hosted in a Ribbon we want to set null logical parent because panel children already have logical parent
            return new UIElementCollection(this, TemplatedParent == null ? logicalParent : null);
        }

        protected override bool HasLogicalOrientation
        {
            get { return false; }
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _separatorOpacity = 0.0;
            Size desiredSize = new Size();

            int countAllTabs = Children.Count;
            if (countAllTabs == 0)
            {
                return desiredSize;
            }

            int countRegularTabs = Ribbon.Tabs.Count;
            int countContextualTabs = Ribbon.ContextualTabs.Count;
            Debug.Assert(countRegularTabs + countContextualTabs == countAllTabs, "Regular and Contextual tabs should be the same as Children count.");

            double totalDefaultPaddingAllTabs = 0;
            double totalDefaultPaddingRegularTabs = 0;
            bool showRegularTabsToolTips = false;
            bool showContextualTabsToolTips = false;

            double totalDesiredWidthRegularTabs = 0;

            // Step 1: Measure all tabs to fit their content
            // desiredSize should hold the total size required to fit all tabs
            for (int i = 0; i < countAllTabs; i++)
            {
                RibbonTab ribbonTab = Children[i] as RibbonTab;
                Debug.Assert(ribbonTab != null);
                if (ribbonTab != null)
                {
                    ribbonTab.Padding = ribbonTab.DefaultPadding; // Always do first meassure with default padding
                    double tabPadding = ribbonTab.DefaultPadding.Left + ribbonTab.DefaultPadding.Right;
                    totalDefaultPaddingAllTabs += tabPadding;

                    // Regular Tab
                    if (i < countRegularTabs)
                    {
                        totalDefaultPaddingRegularTabs += tabPadding;
                    }

                    ribbonTab.Measure(constraint);
                    desiredSize.Width += ribbonTab.DesiredSize.Width;
                    desiredSize.Height = Math.Max(desiredSize.Height, ribbonTab.DesiredSize.Height);
                }

                if (i == countRegularTabs - 1)
                {
                    totalDesiredWidthRegularTabs = desiredSize.Width; // Remember the width of the regular tabs
                }
            }

            // Step 2: If tabs don't fit into the constraint
            // Reduce Tab padding and re-measure
            double overflowWidth = desiredSize.Width - constraint.Width; // Total overflow width
            if (overflowWidth > 0)
            {
                // Calculate max tab width if tab clipping is necessary
                double totalClipWidthRegularTabs = 0; // How much pixels we need to clip regular tabs
                double totalClipWidthContextualTabs = 0; // How much pixels we need to clip contextual tabs

                // Clipping is necessary - all tabs padding will we 0
                if (overflowWidth > totalDefaultPaddingAllTabs) 
                {
                    showRegularTabsToolTips = true;
                    totalClipWidthRegularTabs = overflowWidth - totalDefaultPaddingAllTabs; // Try to use the whole totalClipAmount in the regular tabs
                }

                double maxRegularTabWidth = CalculateMaxTabWidth(totalClipWidthRegularTabs, Ribbon.Tabs);
                if (maxRegularTabWidth == _tabMinWidth) // Regular tabs are clipped to the min size -  need to clip contextual tabs
                {
                    showContextualTabsToolTips = true;
                    double totalClipAmount = overflowWidth - totalDefaultPaddingAllTabs;
                    double usedClipAmount = totalDesiredWidthRegularTabs - totalDefaultPaddingRegularTabs - (_tabMinWidth * countRegularTabs);
                    totalClipWidthContextualTabs = totalClipAmount - usedClipAmount; // Remaining clipping amount
                }

                double maxContextualTabWidth = CalculateMaxTabWidth(totalClipWidthContextualTabs, Ribbon.ContextualTabs);

                double reducePaddingRegularTab = 0;
                double reducePaddingContextualTab = 0;
                desiredSize = new Size();
                if (totalDefaultPaddingRegularTabs >= overflowWidth) // overflow can be covered by reducing regular tab padding only
                {
                    reducePaddingRegularTab = (0.5 * overflowWidth) / countRegularTabs;
                    _separatorOpacity = Math.Max(0.0, reducePaddingRegularTab * 0.2);
                }
                else // Need to reduce contextual tab padding also
                {
                    _separatorOpacity = 1.0;
                    reducePaddingRegularTab = double.PositiveInfinity;

                    // If countContextualTabs==0 then reducePaddingContextualTab will become Infinity
                    reducePaddingContextualTab = (0.5 * (overflowWidth - totalDefaultPaddingRegularTabs)) / countContextualTabs;
                }

                for (int i = 0; i < countAllTabs; i++)
                {
                    RibbonTab ribbonTab = Children[i] as RibbonTab;
                    if (ribbonTab != null)
                    {
                        bool isRegularTab = i < countRegularTabs;
                        double leftpadding = Math.Max(0, ribbonTab.DefaultPadding.Left - (isRegularTab ? reducePaddingRegularTab : reducePaddingContextualTab));
                        double rightpadding = Math.Max(0, ribbonTab.DefaultPadding.Right - (isRegularTab ? reducePaddingRegularTab : reducePaddingContextualTab));
                        ribbonTab.Padding = new Thickness(leftpadding, ribbonTab.DefaultPadding.Top, rightpadding, ribbonTab.DefaultPadding.Bottom);

                        ribbonTab.Measure(new Size(isRegularTab ? maxRegularTabWidth : maxContextualTabWidth, constraint.Height));

                        desiredSize.Width += ribbonTab.DesiredSize.Width;
                        desiredSize.Height = Math.Max(desiredSize.Height, ribbonTab.DesiredSize.Height);
                    }
                }
            }

            // Update whether tooltips should be shown.
            UpdateToolTips(showRegularTabsToolTips, showContextualTabsToolTips);
            
            VerifyScrollData(constraint.Width, desiredSize.Width);

            return desiredSize;
        }

        private void UpdateToolTips(bool showRegularTabsToolTips, bool showContextualTabsToolTips)
        {
            int countAllTabs = Children.Count;
            int countRegularTabs = Ribbon.Tabs.Count;
            for (int i = 0; i < countAllTabs; i++)
            {
                bool isRegularTab = i < countRegularTabs;
                RibbonTab ribbonTab = Children[i] as RibbonTab;
                if (ribbonTab != null)
                {
                    ribbonTab.ShowLabelToolTip = isRegularTab ? showRegularTabsToolTips : showContextualTabsToolTips;
                }
            }
        }

        protected override Size ArrangeOverride(Size arrangeSize)
        {
            double x = 0.0;
            foreach (UIElement child in Children)
            {
                child.Arrange(new Rect(x - HorizontalOffset, arrangeSize.Height - child.DesiredSize.Height, child.DesiredSize.Width, child.DesiredSize.Height));
                x += child.DesiredSize.Width;
            }

            // Update RibbonTabGroup widths for active groups
            if (Ribbon.ContextualTabs.Count > 0)
            {
                foreach (RibbonContextualTabGroup tabGroup in Ribbon.ContextualTabGroups)
                {
                    if (tabGroup.IsActive)
                    {
                        // If we have horizontal scrollers - hide the contextual group labels
                        if (DoubleUtil.LessThan(ViewportWidth, ExtentWidth))
                        {
                            tabGroup.DesiredWidth = 0.0;
                        }
                        else
                        {
                            double groupWidth = 0.0;
                            foreach (RibbonTab tab in tabGroup.Tabs)
                            {
                                // Doublecheck that tab is visually connected
                                if (Ribbon.ContextualTabs.Contains(tab)) 
                                {
                                    groupWidth += tab.DesiredSize.Width;
                                }
                            }

                            tabGroup.DesiredWidth = groupWidth;
                        }

                        tabGroup.ArrangeWidth = 0;
                        tabGroup.ArrangeX = 0;
                    }
                }
            }

            // this arrange happens after the RibbonTitlePanel arrange so we need to update the RibbonGroup positions whenever RibbonTabs move around
            Ribbon.RibbonTitlePanel.InvalidateMeasure();
            InvalidateVisual();

            return arrangeSize;
        }

        // This method determine how much tabs will be clipped by caluclating the maximum tab width
        // clipWidth parameter is the amount the needs to be removed from tabs
        // Algorithm steps:
        // 1. Sort all tabs sizes
        // 2. maxTabWidth = max tab size - clipWidth
        // 3. if there is an element bigger that maxTabWidth - include this element and calulate new average
        // 4. Return maxTabWidth coerced with some min width
        private double CalculateMaxTabWidth(double clipWidth, Collection<RibbonTab> tabs)
        {
            // If clipping is not necessary - return Max double
            if (clipWidth <= 0)
            {
                return Double.MaxValue;
            }

            int count = tabs.Count;

            // Sort element sizes without the padding
            List<double> elementSizes = new List<double>(count);
            foreach (RibbonTab tab in tabs)
            {
                elementSizes.Add(tab.DesiredSize.Width - tab.DefaultPadding.Left - tab.DefaultPadding.Right);
            }

            elementSizes.Sort();

            // Clip the max element
            double maxTabWidth = elementSizes[count - 1] - clipWidth;
            for (int i = 1; i < count; i++)
            {
                double currentWidth = elementSizes[count - 1 - i];
                if (maxTabWidth >= currentWidth)
                {
                    break;
                }

                // Include next element and calculate new average
                maxTabWidth = ((maxTabWidth * i) + currentWidth) / (i + 1);
            }

            return Math.Max(_tabMinWidth, maxTabWidth);
        }

        // Verifies scrolling data using the passed viewport and extent as newly computed values.
        // Checks the X/Y offset and coerces them into the range [0, Extent - ViewportSize]
        // If extent, viewport, or the newly coerced offsets are different than the existing offset,
        //   cachces are updated and InvalidateScrollInfo() is called.
        private void VerifyScrollData(double viewportWidth, double extentWidth)
        {
            bool fValid = true;

            if (Double.IsInfinity(viewportWidth))
            {
                viewportWidth = extentWidth;
            }

            double offsetX = CoerceOffset(ScrollData._offsetX, extentWidth, viewportWidth);

            fValid &= DoubleUtil.AreClose(viewportWidth, ScrollData._viewportWidth);
            fValid &= DoubleUtil.AreClose(extentWidth, ScrollData._extentWidth);
            fValid &= DoubleUtil.AreClose(ScrollData._offsetX, offsetX);

            ScrollData._viewportWidth = viewportWidth;
            ScrollData._extentWidth = extentWidth;
            ScrollData._offsetX = offsetX;

            if (!fValid)
            {
                if (ScrollOwner != null)
                {
                    ScrollOwner.InvalidateScrollInfo();
                }
            }
        }

        // Returns an offset coerced into the [0, Extent - Viewport] range.
        // Internal because it is also used by other Avalon ISI implementations (just to avoid code duplication).
        internal static double CoerceOffset(double offset, double extent, double viewport)
        {
            if (offset > extent - viewport) 
            { 
                offset = extent - viewport; 
            }

            if (offset < 0) 
            { 
                offset = 0; 
            }

            return offset;
        }

        protected override void OnRender(DrawingContext dc)
        {
            int count = Children.Count;
            if (_separatorOpacity > 0)
            {
                double xOffset = -HorizontalOffset;
                Brush b = ((Brush)FindResource(RibbonSkinResources.RibbonTabSeparatorBrushKey)).Clone();
                b.Opacity = _separatorOpacity;
                Pen pen = new Pen(b, 1.0);
                for (int i = 0; i < count - 1; i++)
                {
                    UIElement child = Children[i];
                    xOffset += child.DesiredSize.Width;

                    dc.DrawLine(pen, new Point(xOffset, 0), new Point(xOffset, this.ActualHeight));
                }
            }

            base.OnRender(dc);
        }

        private Ribbon Ribbon
        {
            get
            {
                return TemplatedParent as Ribbon;
            }
        }

        private double _separatorOpacity;
        private const double _tabMinWidth = 30.0;

        #region IScrollInfo Members

        public ScrollViewer ScrollOwner
        {
            get { return ScrollData._scrollOwner; }
            set { ScrollData._scrollOwner = value; }
        }

        public void SetHorizontalOffset(double offset)
        {
            double newValue = ValidateInputOffset(offset, "HorizontalOffset");
            if (!DoubleUtil.AreClose(ScrollData._offsetX, newValue))
            {
                _scrollData._offsetX = newValue;
                InvalidateArrange();
            }
        }

        public double ExtentWidth
        {
            get { return ScrollData._extentWidth; }
        }

        public double HorizontalOffset
        {
            get { return ScrollData._offsetX; }
        }

        public double ViewportWidth
        {
            get { return ScrollData._viewportWidth; }
        }

        public void LineLeft()
        {
            SetHorizontalOffset(HorizontalOffset - 16.0);
        }

        public void LineRight()
        {
            SetHorizontalOffset(HorizontalOffset + 16.0);
        }

        // This is optimized for horizontal scrolling only
        public Rect MakeVisible(Visual visual, Rect rectangle)
        {
            // We can only work on visuals that are us or children.
            // An empty rect has no size or position.  We can't meaningfully use it.
            if (rectangle.IsEmpty
                || visual == null
                || visual == (Visual)this
                || !this.IsAncestorOf(visual))
            {
                return Rect.Empty;
            }

            // Compute the child's rect relative to (0,0) in our coordinate space.
            GeneralTransform childTransform = visual.TransformToAncestor(this);

            rectangle = childTransform.TransformBounds(rectangle);

            // Initialize the viewport
            Rect viewport = new Rect(HorizontalOffset, rectangle.Top, ViewportWidth, rectangle.Height);
            rectangle.X += viewport.X;

            // Compute the offsets required to minimally scroll the child maximally into view.
            double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right);

            // We have computed the scrolling offsets; scroll to them.
            SetHorizontalOffset(minX);

            // Compute the visible rectangle of the child relative to the viewport.
            viewport.X = minX;
            rectangle.Intersect(viewport);

            rectangle.X -= viewport.X;

            // Return the rectangle
            return rectangle;
        }

        internal static double ComputeScrollOffsetWithMinimalScroll(
            double topView,
            double bottomView,
            double topChild,
            double bottomChild)
        {
            // # CHILD POSITION       CHILD SIZE      SCROLL      REMEDY
            // 1 Above viewport       <= viewport     Down        Align top edge of child & viewport
            // 2 Above viewport       > viewport      Down        Align bottom edge of child & viewport
            // 3 Below viewport       <= viewport     Up          Align bottom edge of child & viewport
            // 4 Below viewport       > viewport      Up          Align top edge of child & viewport
            // 5 Entirely within viewport             NA          No scroll.
            // 6 Spanning viewport                    NA          No scroll.
            //
            // Note: "Above viewport" = childTop above viewportTop, childBottom above viewportBottom
            //       "Below viewport" = childTop below viewportTop, childBottom below viewportBottom
            // These child thus may overlap with the viewport, but will scroll the same direction
            bool fAbove = DoubleUtil.LessThan(topChild, topView) && DoubleUtil.LessThan(bottomChild, bottomView);
            bool fBelow = DoubleUtil.GreaterThan(bottomChild, bottomView) && DoubleUtil.GreaterThan(topChild, topView);
            bool fLarger = (bottomChild - topChild) > (bottomView - topView);

            // Handle Cases:  1 & 4 above
            if ((fAbove && !fLarger)
               || (fBelow && fLarger))
            {
                return topChild;
            }

            // Handle Cases: 2 & 3 above
            else if (fAbove || fBelow)
            {
                return bottomChild - (bottomView - topView);
            }

            // Handle cases: 5 & 6 above.
            return topView;
        }

        // Does not support other scrolling than LineLeft/LineRight
        public void MouseWheelDown() 
        { 
        }
        
        public void MouseWheelLeft() 
        { 
        }
        
        public void MouseWheelRight() 
        { 
        }
        
        public void MouseWheelUp() 
        { 
        }
        
        public void LineDown() 
        { 
        }
        
        public void LineUp() 
        { 
        }
        
        public void PageDown() 
        { 
        }
        
        public void PageLeft() 
        {
        }
        
        public void PageRight() 
        { 
        }
        
        public void PageUp() 
        {
        }

        public void SetVerticalOffset(double offset) 
        { 
        }

        public bool CanVerticallyScroll
        {
            get { return false; }
            set { }
        }

        public bool CanHorizontallyScroll
        {
            get { return true; }
            set { }
        }

        public double ExtentHeight
        {
            get { return 0.0; }
        }

        public double VerticalOffset
        {
            get { return 0.0; }
        }

        public double ViewportHeight
        {
            get { return 0.0; }
        }

        private ScrollData ScrollData
        {
            get
            {
                return _scrollData ?? (_scrollData = new ScrollData());
            }
        }

        private ScrollData _scrollData;

        internal static double ValidateInputOffset(double offset, string parameterName)
        {
            if (double.IsNaN(offset))
            {
                throw new ArgumentOutOfRangeException(parameterName);
            }

            return Math.Max(0.0, offset);
        }

        #endregion
    }

    //-----------------------------------------------------------
    // ScrollData class
    //-----------------------------------------------------------
    #region ScrollData

    // Helper class to hold scrolling data.
    // This class exists to reduce working set when SCP is delegating to another implementation of ISI.
    // Standard "extra pointer always for less data sometimes" cache savings model:
    // TODO: Consider using the Stack internal helper.  It's more or less the same.
    internal class ScrollData
    {
        internal ScrollViewer _scrollOwner;

        internal double _offsetX;

        internal double _viewportWidth; // ViewportSize is computed from our FinalSize, but may be in different units.
        internal double _extentWidth; // Extent is the total size of our content.
    }

    #endregion ScrollData

    internal static class DoubleUtil
    {
        // Fields
        internal const double DBL_EPSILON = 2.2204460492503131E-16;

        public static bool AreClose(double value1, double value2)
        {
            if (value1 == value2)
            {
                return true;
            }

            double num = ((Math.Abs(value1) + Math.Abs(value2)) + 10.0) * DBL_EPSILON;
            double num2 = value1 - value2;
            return (-num < num2) && (num > num2);
        }

        public static bool GreaterThan(double value1, double value2)
        {
            return (value1 > value2) && !AreClose(value1, value2);
        }

        public static bool LessThan(double value1, double value2)
        {
            return (value1 < value2) && !AreClose(value1, value2);
        }
    }
}